import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
import java.util.Random;
import java.util.HashMap;
import java.util.TreeSet;

public class IndirectBackprop {
	
	private NeuralNet substrate;
	private NeuralNet cppn;
	private static double[][][] data;
	private static double[][][] test;
	
	public IndirectBackprop(String[][] layoutS, String[][] layoutC) {
		substrate = new NeuralNet(NeuralNet.quickSubstrate(layoutS), 0);
		data = 	trainingData();
		test = testData();
		//cppn = new NeuralNet(NeuralNet.quickCPPN(layoutC), 1);
		double e;
		do {
			cppn = new NeuralNet(NeuralNet.quickCPPN(layoutC), 1);
			setSubstrateWeights(cppn, substrate);
			e = substrate.evaluate(data) / data.length;
			System.out.println("Start: "+e);
		} while(e > 1.0);
	}

	public static void main(String[] args) {
		String[][] layoutS = {{"11","11","L"},{"11","11","S"}};
		//String[][] layoutC = {{"B", "L", "L", "L", "L", "L", "L"},{"S", "L"}, {"G", "G"}, {"G", "G"}, {"S"}};
		String[][] layoutC = {{"B", "L"}, {"G", "G"}, {"S"}};
		IndirectBackprop net = new IndirectBackprop(layoutS, layoutC);
		
		net.interleaved(3);
		//net.hillClimbReg(100);
		
		net.testBoxes();
		System.out.println("");
		net.boxesDistance(data);
		System.out.println("");
		net.boxesDistance(test);
		
		System.out.println("");
		double[] i = new double[1];
		double[] v = {1, Math.sqrt(2), 2, Math.sqrt(5), Math.sqrt(8), 3};
		for (int j=0; j < v.length; j++) {
			i[0] = v[j];
			net.cppn.processInput(i);
			System.out.println(net.cppn.getOutputs()[0]);
		}
	}
	
	public void interleaved(int thres) {
		int cppnFactor = 100;
		double e;
		setSubstrateWeights(cppn, substrate);
		do {
			for (int i = 0; i < thres; i++) {
				e = substrate.trainNetwork(data, 1.0) / data.length;
				System.out.println("Substrate: "+ e);
			}
			double[][][] cppnData = cppnData(substrate);
			for (int i = 0; i < cppnFactor*thres; i++) {
				e = cppn.trainNetwork(cppnData, 1.0) / cppnData.length;
				//System.out.println("CPPN: "+ e);
			}
			setSubstrateWeights(cppn, substrate);
			e = substrate.evaluate(data) / data.length;
			System.out.println(e);
		} while (e > 0.1);
	}
	
	public void hillClimb(int thres) {
		//Start at point in cppn space
		setSubstrateWeights(cppn, substrate);
		//Evaluate starting point
		double bestErr = substrate.evaluate(data) / data.length;
		System.out.println("Substrate error: "+bestErr);
		
		//Create substrate clone
		String[][][] con = new String[substrate.size()][][];
		for (int i = 0; i < substrate.size(); i++) {
			con[i] = new String[substrate.size(i)][3];
			for (int j = 0; j < substrate.size(i); j++) {
				Node n = substrate.getNode(i, j);
				con[i][j][0] = "" + n.getCoords()[0];
				con[i][j][1] = "" + n.getCoords()[1];
				con[i][j][2] = n.getType();
			}
		}
		NeuralNet mutation = new NeuralNet(con, 0);
		
		//Create cppn clone
		String[][][] cCon = new String[cppn.size()][][];
		for (int i = 0; i < cppn.size(); i++) {
			cCon[i] = new String[cppn.size(i)][3];
			for (int j = 0; j < cppn.size(i); j++) {
				Node n = cppn.getNode(i, j);
				cCon[i][j][0] = "" + n.getCoords()[0];
				cCon[i][j][1] = "" + n.getCoords()[1];
				cCon[i][j][2] = n.getType();
			}
		}
		NeuralNet cppnClone = new NeuralNet(cCon, 1);
		
		Random r = new Random();
		do {
			//Mutate current substrate weights
			for (int i = 0; i < substrate.size(); i++) {
				for (int j = 0; j < substrate.size(i); j++) {
					Node n = substrate.getNode(i, j);
					for (int k = 0; k < n.size(); k++) {
						double w = n.getWeight(k);
						mutation.getNode(i, j).setWeight(k, w + r.nextGaussian());
					}
				}
			}
			//Copy master cppn weights
			for (int i = 0; i < cppn.size(); i++) {
				for (int j = 0; j < cppn.size(i); j++) {
					Node n = cppn.getNode(i, j);
					for (int k = 0; k < n.size(); k++) {
						cppnClone.getNode(i, j).setWeight(k, n.getWeight(k));
					}
				}
			}
			
			//Create cppn training data
			double[][][] cppnData = cppnData(mutation);
			//Fit cppnClone to mutation
			double cppnErr = Double.POSITIVE_INFINITY;
			for (int i = 0; i < thres; i++) {
				double temp = cppnClone.trainNetwork(cppnData, 1.0) / cppnData.length;
				//Sanity check
				if (temp > cppnErr) {
					System.out.println("Warning: Error-rate diverging, lower learning rate");
				}
				cppnErr = temp;
				if (i == 0 || i == thres-1) {
					System.out.println("CPPN: "+ cppnErr);
				}
			}
			setSubstrateWeights(cppnClone, mutation);
			double newErr = mutation.evaluate(data) / data.length;
			System.out.println("Error: "+ newErr + " / "+(newErr < bestErr));
			if (newErr < bestErr) {
				//Swap cppns
				NeuralNet temp = cppn;
				cppn = cppnClone;
				cppnClone = temp;
				//Swap substrate
				temp = substrate;
				substrate = mutation;
				mutation = temp;
				
				bestErr = newErr;
			}
			cppnData = cppnData(substrate);
			System.out.println("Zero: "+cppn.evaluate(cppnData)/cppnData.length);
		} while (bestErr > 1);
	}
	
	public static double[][][] trainingData() {
		double[] v = new double[10];
		for (int i = 0; i < v.length; i++) {
			v[i] = 1.0 / (1 + Math.exp(i*-0.4));
		}
		double[][] patLarge = {{v[1], v[2], v[3], v[2], v[1]},
								{v[2], v[4], v[6], v[4], v[2]},
								{v[3], v[6], v[9], v[6], v[3]},
								{v[2], v[4], v[6], v[4], v[2]},
								{v[1], v[2], v[3], v[2], v[1]}};
		double[][] patSmall = {{v[1], v[1], v[1]},
								{v[1], v[1], v[1]},
								{v[1], v[1], v[1]}};
		
		double[][][] r = new double[75][3][121];
		int k = 0;
		for (int i = 1; i < 11; i+=2) {
			for (int j = 1; j < 11; j+=2) {
				for (int n = 0; n < 3; n++) {
					//Input
					for (int a = -1; a < 2; a++) {
						for (int b = -1; b < 2; b++) {
							r[k+n][0][(i+a)*11 + j+b] = 1;
						}
					}
					int xOff = i;
					int yOff = j;
					if (n == 0) {
						xOff = (i+5)%11;
					} else if (n == 1) {
						yOff = (j+5)%11;
					} else if (n == 2) {
						xOff = (i+5)%11;
						yOff = (j+5)%11;
					}
					r[k+n][0][xOff*11 + yOff] = 1;
					//Output
					//r[k+n][1][i*11 + j] = 1;
					for (int a = 0; a < 11; a++) {
						for (int b = 0; b < 11; b++) {
							r[k+n][1][a*11 + b] = 0.5;
						}
					}
					for (int a = Math.max(i - 2,0); a <= Math.min(i + 2,10); a++) {
						for (int b = Math.max(j - 2,0); b <= Math.min(j + 2,10); b++) {
							r[k+n][1][a*11 + b] = patLarge[a-(i-2)][b-(j-2)];
						}
					}
					for (int a = Math.max(xOff - 1,0); a <= Math.min(xOff + 1,10); a++) {
						for (int b = Math.max(yOff - 1,0); b <= Math.min(yOff + 1,10); b++) {
							r[k+n][1][a*11 + b] = patSmall[a-(xOff-1)][b-(yOff-1)];
						}
					}
					
					//Indexes
					for (int a = 0; a < 121; a++) {
						r[k+n][2][a] = a;
					}
				}
				k+=3;
			}
		}
		return r;
	}
	
	public static double[][][] testData() {
		double[][][] r = new double[9072][3][121];
		int k = 0;
		for (int i = 1; i < 10; i++) {
			for (int j = 1; j < 10; j++) {
				for (int n = 0; n < 11; n++) {
					for (int m = 0; m < 11; m++) {
						//Input
						boolean f = true;
						for (int a = -1; a < 2; a++) {
							for (int b = -1; b < 2; b++) {
								r[k][0][(i+a)*11 + j+b] = 1;
								if (i+a == n && j+b == m) {
									f = false;
								}
							}
						}
						//Output
						r[k][1][i*11 + j] = 1;
						//Indexes
						for (int a = 0; a < 121; a++) {
							r[k][2][a] = a;
						}
						//Small box
						if (f) {
							r[k][0][n*11 + m] = 1;
							k++;
							if (k > 9071) {
								return r;
							}
						}
					}
				}
			}
		}
		return r;
	}
	
	//Note: No fix for bias nodes yet
	public double[][][] cppnData(NeuralNet net) {
		ArrayList<double[][]> d = new ArrayList<double[][]>();
		for (int i = 0; i < net.size(); i++) {
			for (int j = 0; j < net.size(i); j++) {
				Node n = net.getNode(i, j);
				for (int k = 0; k < n.size(); k++) {
					Node m = n.getConnection(k);
					//double[][] e = {{/*n.getCoords()[0], n.getCoords()[1], m.getCoords()[0],
					//		m.getCoords()[1], */n.getCoords()[0]-m.getCoords()[0],
					//		n.getCoords()[1]-m.getCoords()[1]},{n.getWeight(k)},{i}};
					double[][] e = {{Math.sqrt(Math.pow(n.getCoords()[0]-m.getCoords()[0],2)+
							Math.pow(n.getCoords()[1]-m.getCoords()[1],2))},{n.getWeight(k)},{i}};
					d.add(e);
				}
			}
		}
		double[][][] r = new double[d.size()][][];
		return d.toArray(r);
	}
	
	//Use CPPN c to set the weights of the substrate net
	public static void setSubstrateWeights(NeuralNet c, NeuralNet net){
		for (int i = 0; i < net.size(); i++) {
			for (int j = 0; j < net.size(i); j++) {
				Node n = net.getNode(i, j);
				for (int k = 0; k < n.size(); k++) {
					Node m = n.getConnection(k);
					//double[] input = {/*n.getCoords()[0], n.getCoords()[1], 
					//		m.getCoords()[0], m.getCoords()[1], */n.getCoords()[0]-m.getCoords()[0],
					//		n.getCoords()[1]-m.getCoords()[1]};
					double[] input = {Math.sqrt(Math.pow(n.getCoords()[0]-m.getCoords()[0],2)+
							Math.pow(n.getCoords()[1]-m.getCoords()[1],2))};
					c.processInput(input);
					n.setWeight(k, c.getOutputs()[i]);
				}
			}
		}
	}
	
	public void boxesDistance(double[][][] data) {
		HashMap<Double, Integer> store = new HashMap<Double, Integer>();
		for (int i = 0; i < data.length; i++) {
			substrate.processInput(data[i][0]);
			double[] o = substrate.getOutputs();
			int m = 0;
			for (int j = 0; j < o.length; j++) {
				if (o[j] > o[m]) {
					m = j;
				}
			}
			double[] c = substrate.getNode(substrate.size() - 1, m).getCoords();
			m = 0;
			for (int j = 0; j < data[i][1].length; j++) {
				if (data[i][1][j] >= data[i][1][m]) {
					m = j;
				}
			}
			int[] d = new int[2];
			d[1] = m % 11;
			d[0] = (m - d[1])/11;
			double dist =  Math.sqrt(Math.pow(c[0]-d[0],2) + Math.pow(c[1]-d[1],2));
			if (store.containsKey(dist)) {
				int count = store.get(dist);
				store.put(dist, count+1);
			} else {
				store.put(dist, 1);
			}
		}
		
		TreeSet<Double> k = new TreeSet<Double>();
		k.addAll(store.keySet());
		Iterator<Double> iter = k.iterator();
		while(iter.hasNext()) {
			double v = iter.next();
			System.out.println(v+": "+store.get(v));
		}
	}
	
	public void testBoxes() {
		int[] x = {7, 21, 46};
		for (int i = 0; i < x.length; i++) {
			boxWrite(data[x[i]][0], "train_input_"+i);
			boxWrite(data[x[i]][1], "train_target_"+i);
			substrate.processInput(data[x[i]][0]);
			boxWrite(substrate.getOutputs(), "train_output_"+i);
			
			boxWrite(test[x[i]][0], "test_input_"+i);
			substrate.processInput(test[x[i]][0]);
			boxWrite(substrate.getOutputs(), "test_output_"+i);
		}
	}
	
	public void boxWrite(double[] box, String add) {
		double sq = 10;
		String sep = String.format(Locale.US,"%6f",0.0)+",";
		try {
			BufferedWriter w = new BufferedWriter(new FileWriter("box_"+add+".csv"));
			for (int i = 0; i < 11*(sq+1)+1; i++) {
				w.write(sep);
			}
			w.write("\n");
			for (int i = 0; i < 11; i++) {
				for (int k = 0; k < sq; k++) {
					w.write(sep);
					for (int j = 0; j < 11; j++) {
						String v = String.format(Locale.US,"%6f",1.0 - box[i*11+j])+",";
						for (int l = 0; l < sq; l++) {
							w.write(v);
						}
						w.write(sep);
					}
					w.write("\n");
				}
				for (int l = 0; l < 11*(sq+1)+1; l++) {
					w.write(sep);
				}
				w.write("\n");
			}
			w.close();
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}
}
